package com.gusto.engine.recommend.prediction.base;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;

import com.gusto.engine.colfil.Distance;
import com.gusto.engine.colfil.Evaluation;
import com.gusto.engine.colfil.Prediction;
import com.gusto.engine.colfil.service.CollaborativeService;
import com.gusto.engine.recommend.delagate.ColNeighborhoodDelegate;
import com.gusto.engine.recommend.delagate.SemNeighborhoodDelegate;

/**
 * <p>Base class for Recommendation algorithms,
 * it provides the basis methods for calculating a prediction,
 * the mean evaluation, the construction of weights array, etc.</p>
 * 
 * @author amokrane.belloui@gmail.com
 */
public abstract class BaseImpl {
	
	private Logger log = Logger.getLogger(getClass());
	
	protected CollaborativeService collaborativeService;
	public void setCollaborativeService(CollaborativeService collaborativeService) {
		this.collaborativeService = collaborativeService;
	}
		
	protected ColNeighborhoodDelegate collaborativeNeighborhoodDelegate;
	public void setCollaborativeNeighborhoodDelegate(ColNeighborhoodDelegate collaborativeNeighborhoodDelegate) {
		this.collaborativeNeighborhoodDelegate = collaborativeNeighborhoodDelegate;
	}
	
	protected SemNeighborhoodDelegate semanticNeighborhoodDelegate;
	public void setSemanticNeighborhoodDelegate(SemNeighborhoodDelegate semanticNeighborhoodDelegate) {
		this.semanticNeighborhoodDelegate = semanticNeighborhoodDelegate;
	}
	
	private boolean logPredicion = false;
	
	protected double getDateWeight(Date date) {
		long dateCurrent = 893286638000L; //en 1998
		long dateRating = date.getTime();
		long diff = (dateCurrent - dateRating) / (1000 * 60 * 60 * 24) ;
		double w = 0;
		if (diff < 30) {
			w = 0.95;
		}
		else if (diff < 60) {
			w = 0.9;
		}
		else if (diff < 90) {
			w = 0.85;
		}
		else if (diff < 120) {
			w = 0.8;
		}
		else if (diff < 180) {
			w = 0.75;
		}
		else if (diff < 240) {
			w = 0.7;
		}
		else {
			w = 0.5;
		}
		log.debug("Date weight " + date + " : " + w);
		return w;
	}
	
	protected Map<Long, Double> buildUserWeights(Long userId, List<Distance> users) {
		Map<Long, Double> usersWeights = new HashMap<Long, Double>();
		
		log.debug("Building weights for user " + userId + ", w/ distances " + users);
		
		///long[] usersIds = new long[users.size() + 1];
		
		////String outputUsers = "";
		////outputUsers += "users: ";
		int i = 0;
		for (Distance d : users) {
			Long uid = null;
			if (userId.equals(d.getId1())) {
				///usersIds[i] = d.getId2();
				uid = d.getId2();
			}
			else {
				///usersIds[i] = d.getId1();
				uid = d.getId1();
			}
			// FIXME Normalize distance
			//usersWeights.put(usersIds[i], (d.getDistance().getDistance() + 1) / 2);
			///usersWeights.put(usersIds[i], d.getDistance());
			usersWeights.put(uid, d.getDistance());
			////outputUsers += usersIds[i] + "(" + d.getDistance() + ")" + " ";
			i++;
		}
		///usersIds[i] = userId;
		///usersWeights.put(usersIds[i], 1.0);
		usersWeights.put(userId, 1.0);
		////outputUsers += usersIds[i] + " ";
		/*
		if (logPredicion) {
			////log.debug("User [" + userId + "] weights : " + outputUsers);
		}
		*/
		return usersWeights;
	}
	
	protected Map<Long, Double> buildItemWeights(Long itemId, List<Distance> items) {
		Map<Long, Double> itemsWeights = new HashMap<Long, Double>();
		
		log.debug("Building weights for item " + itemId + ", w/ distances " + items);
		
		///long[] itemsIds = new long[items.size() + 1];
		
		////String outputItems = "items: ";
		int i = 0;
		for (Distance d : items) {
			Long iid = null;
			if (itemId.equals( d.getId1())) {
				///itemsIds[i] = d.getId2();
				iid = d.getId2();
			}
			else {
				///itemsIds[i] = d.getId1();
				iid = d.getId1();
			}
			// FIXME Normalize distance
			//itemsWeights.put(itemsIds[i], (d.getDistance().getDistance() + 1) / 2);
			///itemsWeights.put(itemsIds[i], d.getDistance());
			itemsWeights.put(iid, d.getDistance());
			////outputItems += itemsIds[i] + "(" + d.getDistance() + ")" + " ";
			i++;
		}
		///itemsIds[i] = itemId;
		///itemsWeights.put(itemsIds[i], 1.0);
		itemsWeights.put(itemId, 1.0);
		////outputItems += itemsIds[i] + " ";
		/*
		if (logPredicion) {
			////log.debug("Item [" + itemId + "] weights : " + outputItems);
		}
		*/
		return itemsWeights;
	}
	
	protected Double buildNeihborhoodQuality(List<Distance> distances) {
		Double totalCommon = 0.0;
		Double count = 0.0;
		for (Distance d : distances) {
			totalCommon += d.getCount();
			count ++;
		}
		return totalCommon/count;
	}
	
	
	protected Double buildMeanPrediction(List<Evaluation> evals, double user_mean, double item_mean, boolean includePrediction) {
		int count = 0;
		double total = 0.0;
		for (Evaluation e : evals) {
			total += e.getValue();
			count++;
		}
		double val = total / (double)count;
		
		// Evaluation matrix has been normalized, 
		//de-normalize it with user mean and (item mean);
		val = val + ((user_mean + item_mean)/2);
		log.debug("Mean prediction [" + user_mean + "] [" + item_mean + "]");
		return val;
	}
	
	protected Double buildPrediction(List<? extends Evaluation> evals, double user_mean, double item_mean, Map<Long, Double> usersWeights, Map<Long, Double> itemsWeights) {
		double totalWeighted = 0.0;
		double sumWeight = 0.0;
		for (Evaluation e : evals) {
			log.debug(e.getUserId() + "-" + e.getItemId() + ": " + e.getValue());
			
			double weight = usersWeights.get(e.getUserId()) * itemsWeights.get(e.getItemId());
			// rating date weight
			// double weight = getDateWeight(e.getTimeStamp()) * usersWeights.get(e.getUserId()) * itemsWeights.get(e.getItemId());
			totalWeighted += weight * e.getValue();
			sumWeight += weight;
		}
		
		double val = totalWeighted / (double)sumWeight;
		log.debug("Build Prediction (Normalized) : " + totalWeighted + "/" + sumWeight + " = " + val);
		
		// Evaluation matrix has been normalized, 'denormalize it with user mean and (item mean);
		// FIXME Normalized or not
		//val = val + ((user_mean + item_mean)/2);
		Double valF = (val + ((user_mean + item_mean)/2)) / 2;
		log.debug("Build Prediction : user mean " + user_mean + " item mean " + item_mean + " => " + val + " = " + valF);
		
		return valF;
	}
	
	protected Prediction returnPrediction(long userId, long itemId, Double prediction) {
		Prediction pred = new Prediction(prediction);
		//eval = new Evaluation( Math.round(total / (double)count * 10) / 10.0);
		pred.setUserId(userId);
		pred.setItemId(itemId);
		return pred;
	}
	
	protected long checkpoint1 = System.currentTimeMillis();
	protected void logPrediction(double evals, long users, long items, long timeMillis, String message) {
		if (logPredicion) {
			log.debug("PRED u:" + users + " i:" + items + ", m:" + evals + " (" + (timeMillis*1.0/1000) + ")");
			log.debug(message);
		}
		if ((System.currentTimeMillis() - checkpoint1) > 10000) {
			log.info(message);
			checkpoint1 = System.currentTimeMillis();
		}
	}
	
	protected void logCheckPoint(String checkpoint, long timeMillis) {
		if (logPredicion) {
			log.info("# " + checkpoint + " " + (timeMillis*1.0/1000) + "");
		}
	}
	
	
}
